﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using Ppt = Microsoft.Office.Interop.PowerPoint;
using ObjectModel.PowerPoint;
using ObjectModel.PowerPoint.New;
using ObjectModel.Utilities;

namespace ViewComponents.PowerPoint.ViewModels
{
    public class PPTViewModelController
    {
        readonly Ppt.Application mApp = Globals.Application;
        readonly PresentationEvents mEvents;
        readonly LinkedSlideDictionary mSlides;
        readonly IPPTViewModel mViewModel;

        public event EventHandler<BeforeTextRangeDeletedEventArgs> BeforeTextRangeDeleted;

        public PPTViewModelController(Ppt.Presentation pres, LinkedSlideDictionary slides, IPPTViewModel viewModel)
        {
            mSlides = slides;
            mViewModel = viewModel;
            mViewModel.ActiveSlide = mSlides[pres.ActiveSlide().SlideID];

            mEvents = Globals.PresentationEvents[pres];
            Enable();
        }

        LinkedSlide ActiveSlide
        {
            get { return mViewModel.ActiveSlide; }
            set { mViewModel.ActiveSlide = value; }
        }

        LinkedTextRange SelectedTextRange
        {
            get { return mViewModel.ActiveTextRange; }
            set { mViewModel.ActiveTextRange = value; }
        }

        bool TextIsSelected
        {
            get { return mViewModel.TextIsSelected; }
            set { mViewModel.TextIsSelected = value; }
        }

        public void Enable()
        {
            mEvents.SlideActivated += OnSlideActivated;
            mEvents.SlideDeleted += OnSlideDeleted;
            mEvents.ShapeDeleted += OnShapeDeleted;
            mEvents.TextDeselected += OnTextDeselected;
            mEvents.TextSelectionChanged += OnTextSelectionChanged;
        }

        public void Disable()
        {
            mEvents.SlideActivated -= OnSlideActivated;
            mEvents.SlideDeleted -= OnSlideDeleted;
            mEvents.ShapeDeleted -= OnShapeDeleted;
            mEvents.TextDeselected -= OnTextDeselected;
            mEvents.TextSelectionChanged -= OnTextSelectionChanged;
        }

        void OnSlideActivated(object sender, SlideChangedEventArgs e)
        {
            ActiveSlide = mSlides[e.SlideInfo.Id];
        }

        void OnSlideDeleted(object sender, SlideChangedEventArgs e)
        {
            mSlides.Remove(e.SlideInfo.Id);
        }

        void OnShapeDeleted(object sender, ShapeChangedEventArgs e)
        {
            // Remove LinkedTextRanges when parent shape is removed.
            IEnumerable<LinkedTextRange> textRangesToDelete = mSlides[e.ShapeInfo.SlideId].TextRanges.Where(ltr => ltr.ParentShape.Id == e.ShapeInfo.Id).ToArray();
            foreach (LinkedTextRange ltr in textRangesToDelete)
                DeleteLinkedTextRange(ltr);
        }

        void OnTextDeselected(object sender, EventArgs e)
        {
            SelectedTextRange = null;
            TextIsSelected = false;
        }

        void OnTextSelectionChanged(object sender, TextSelectionChangedWithinShapeEventArgs e)
        {
            TextIsSelected = true;

            // Unless the selected item of the TextRanges collection is still selected,
            // set selected item to null.
            if (SelectedTextRange != null && !SelectedTextRange.Selected)
                SelectedTextRange = null;

            ICollection<LinkedTextRange> textRangesToDelete = new Collection<LinkedTextRange>();

            foreach (LinkedTextRange ltr in ActiveSlide.TextRanges)
            {
                // If the TextRange's Text property has changed.
                if (ltr.TextRange.Text != ltr.StoredText)
                {
                    // If the TextRange has been appended to by the user we don't want
                    // that change to be considered part of the TextRange. If this
                    // happens, restore the TextRange to its original reference by
                    // setting it to a subset of itself minus the newly added last character.
                    if (ltr.TextRange.Text.StartsWith(ltr.StoredText) &&
                        ltr.TextRange.Text.Length == ltr.StoredText.Length + 1)
                    {
                        ltr.TextRange = ltr.TextRange.Characters(1, ltr.TextRange.Length - 1);
                    }
                    // Check for PowerPoint AutoCorrect auto-capitalising the first
                    // letter of the TextRange.
                    // Conditions:
                    //    1. TextRange must start at first postion in parent TextFrame.
                    //    2. The first char must be capitalised and the last char must
                    //       be a space.
                    else if (ltr.TextRange.Start == 1 && ltr.TextRange.Text.Length > 0 &&
                        ltr.TextRange.Text.Last() == ' ' &&
                        ltr.TextRange.Text.TrimEnd() == ltr.StoredText.CapitaliseFirstChar())
                    {
                        ltr.TextRange = ltr.TextRange.Characters(1, ltr.TextRange.Length - 1);
                        ltr.TextRange.Text = ltr.StoredText;
                    }
                    // Text range has been wholly or partially deleted.
                    else
                    {
                        if (IsTextRangeDeletionConfirmed(ltr, !ltr.ContainsSelection))
                            textRangesToDelete.Add(ltr);
                        else
                            ltr.UpdateValue();
                    }

                    ltr.PreviouslySelected = false;
                }
                // If TextRange is not already wholly selected.
                else if (!ltr.Selected)
                {
                    // If selection is contained within TextRange.
                    if (ltr.ContainsSelection)
                        OnTextRangeSelected(ltr);

                    else
                        ltr.PreviouslySelected = false;
                }
            }

            foreach (LinkedTextRange ltr in textRangesToDelete)
                DeleteLinkedTextRange(ltr);
        }

        bool IsTextRangeDeletionConfirmed(LinkedTextRange ltr, bool isDelayed = false)
        {
            BeforeTextRangeDeletedEventArgs e = new BeforeTextRangeDeletedEventArgs(ltr, isDelayed);
            OnBeforeTextRangeDeleted(e);
            if (!e.Cancel)
                return true;
            
            return false;
        }

        void DeleteLinkedTextRange(LinkedTextRange linkedTextRange)
        {
            mSlides[linkedTextRange.ParentShape.Slide.Id].TextRanges.Remove(linkedTextRange);
        }

        void OnTextRangeSelected(LinkedTextRange ltr)
        {
            // Note: bearing in mind that this point in code is reached when PowerPoint raises a WindowSelectionChange
            // event, when the Select method is called PowerPoint raises this event again and there seems no way to 
            // avoid this (e.g. by temporarily unhooking events, etc). It appears that PowerPoint raises the WindowSelectionChange
            // event only when the physical selection changes. Thus is doesn't raise the event if this method is called and the physical
            // selection does not change which means unhooking events while this method is called is not
            // necessary to prevent an infinite loop. Therefore this point in code is reached twice whenever
            // the text selection changes in PowerPoint from within a shape that is already selected and I don't think anything
            // can be done about that. Strangely enough this doesn't happen when text is selected in a shape which is not
            // already the currently selected and I haven't been able to figure out why.

            // Define autoselection behaviour of this class' wrapped TextRange:
            // If any part of the TextRange is selected, the selection will be automatically extended
            // to the whole TextRange.
            // If the TextRange is already selected and the cursor is moved to the start of the
            // TextRange (e.g. by pressing the Left key) the cursor will be automatically moved
            // one character to the left.
            // The aim of this behaviour is to ensure that the TextRange is either wholly selected
            // or not selected at all.

            Ppt.TextRange selTextRange = mApp.ActiveWindow.Selection.TextRange;

            // If cursor is at the start of the TextRange keep it there.
            if (selTextRange.Start == ltr.TextRange.Start && selTextRange.Length == 0 &&
                    ltr.PreviouslySelected)
                ltr.ParentTextRange.Characters(ltr.TextRange.Start, 0).Select();

            // If cursor is at the end of the TextRange keep it there.
            else if (selTextRange.Start + selTextRange.Length == ltr.TextRange.Start + ltr.TextRange.Length &&
                     selTextRange.Length == 0 && ltr.PreviouslySelected)
                ltr.ParentTextRange.Characters(ltr.TextRange.Start + ltr.TextRange.Length, 0).Select();

            // Otherwise select the whole TextRange.
            else
                SelectedTextRange = ltr;
        }

        void OnBeforeTextRangeDeleted(BeforeTextRangeDeletedEventArgs e)
        {
            if (BeforeTextRangeDeleted != null)
                BeforeTextRangeDeleted(this, e);
        }
    }
}
